package de.jmda.fx.node;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.input.MouseEvent;

import org.apache.log4j.Logger;

import de.jmda.fx.node.behaviour.BehaviourHighlighting;
import de.jmda.fx.node.behaviour.BehaviourHighlightingOnMouseEntered;
import de.jmda.fx.node.behaviour.BehaviourHighlightingOnMouseExited;
import de.jmda.fx.node.behaviour.BehaviourMouseDragging;
import de.jmda.fx.node.behaviour.BehaviourMouseDraggingEvents;
import de.jmda.fx.node.behaviour.BehaviourMouseDraggingStart;
import de.jmda.fx.node.behaviour.BehaviourMouseDraggingStop;
import de.jmda.fx.node.behaviour.Draggable;
import de.jmda.fx.node.behaviour.DraggingCalculatorDefault;
import de.jmda.fx.node.behaviour.Highlightable;
import de.jmda.fx.node.behaviour.BehaviourMouseDraggingConstraint;
import de.jmda.fx.node.behaviour.DraggingData;
import de.jmda.fx.node.behaviour.DraggingCalculator;

public class Line
		extends javafx.scene.shape.Line
		implements Highlightable, Draggable
{
	private final static Logger LOGGER = Logger.getLogger(Line.class);

	/**
	 * start point of line
	 * <p>
	 * Will be updated when line moves.
	 */
	private ObjectProperty<Point2D> startPoint = new SimpleObjectProperty<Point2D>(new Point2D(0,0));
	public Point2D getStartPoint() { return startPoint.get(); }
	protected void setStartPoint(Point2D startPoint) { this.startPoint.set(startPoint); }
	public ObjectProperty<Point2D> startPointProperty() { return startPoint; }

	/**
	 * end point of line
	 * <p>
	 * Will be updated when line moves.
	 */
	private ObjectProperty<Point2D> endPoint = new SimpleObjectProperty<Point2D>(new Point2D(0,0));
	public Point2D getEndPoint() { return endPoint.get(); }
	protected void setEndPoint(Point2D endPoint) { this.endPoint.set(endPoint); }
	public ObjectProperty<Point2D> endPointProperty() { return endPoint; }

	public Line(double x1, double y1, double x2, double y2)
	{
		// always start at 0,0 so calculate x2 and y2
//		super(0, 0, x2 - x1, y2 - y1);
		super(x1, y1, x2, y2);
		
		setStartPoint(new Point2D(x1, y1));
		setEndPoint(  new Point2D(x2, y2));

		boundsInParentProperty().addListener(new LinePointUpdater<Bounds>());
	}

	@Override
	public void activateDragging()
	{
		BehaviourMouseDraggingData behaviourMouseDraggingData =
				new BehaviourMouseDraggingData();

		new BehaviourMouseDraggingStartLine(this, behaviourMouseDraggingData);
		new BehaviourMouseDraggingStop(     this, behaviourMouseDraggingData);
		new BehaviourMouseDraggingLine(     this, behaviourMouseDraggingData);
	}

	@Override
  public void deactivateDragging()
  {
		LOGGER.warn("not (yet) implemented");
  }

	@Override
	public void activateHighlighting()
	{
		new BehaviourHighlightingOnMouseEntered<MouseEvent>(this, this);
		new BehaviourHighlightingOnMouseExited<MouseEvent>(this, this);
	}

	@Override
  public void deactivateHighlighting()
  {
		LOGGER.warn("not (yet) implemented");
  }

	@Override
  public void highlightingOn()
  {
		setStrokeWidth(getStrokeWidth() + BehaviourHighlighting.STROKE_WIDTH_DIFF);
  }

	@Override
  public void highlightingOff()
  {
		setStrokeWidth(getStrokeWidth() - BehaviourHighlighting.STROKE_WIDTH_DIFF);
  }

	public String coordinatesAsString()
	{
		return
//				"layo xy " + getLayoutX() + " " + getLayoutY() + " " +
				"start xy " + getStartX() + " " + getStartY() + " " +
				"end xy " + getEndX() + " " + getEndY();
	}
	
	/**
	 * updates {@link Line#startPointProperty()} and {@link
	 * Line#endPointProperty()} with new values based on {@link Line#getLayoutX()}
	 * and {@link Line#getLayoutY()}
	 *
	 * @param <T>
	 */
	private class LinePointUpdater<T extends Bounds>
			implements ChangeListener<Bounds>
	{
		@Override
	  public void changed(
	  		ObservableValue<? extends Bounds> observable,
	  		Bounds oldValue, Bounds newValue)
	  {
			startPointProperty().set(
					new Point2D(
							layoutXProperty().get() + startXProperty().get(),
							layoutYProperty().get() + startYProperty().get()));
			endPointProperty().set(
					new Point2D(
							layoutXProperty().get() + endXProperty().get(),
							layoutYProperty().get() + endYProperty().get()));
//			LOGGER.debug(
//					"s: " + startPoint + " e: " + endPoint +
//					" lx: " + layoutXProperty().get() + " ly: " + layoutYProperty().get());
	  }
	}

	/**
	 * Adds attributes to super class that store line's start and end point when
	 * dragging starts.
	 */
	public class BehaviourMouseDraggingData extends BehaviourMouseDraggingEvents
	{
		private Point2D startPointAtDraggingStart;
		private Point2D endPointAtDraggingStart;

		public Point2D getStartPointAtDraggingStart()
    {
    	return startPointAtDraggingStart;
    }
		public void setStartPointAtDraggingStart(Point2D startPointAtDraggingStart)
    {
    	this.startPointAtDraggingStart = startPointAtDraggingStart;
    }
		public Point2D getEndPointAtDraggingStart()
    {
    	return endPointAtDraggingStart;
    }
		public void setEndPointAtDraggingStart(Point2D endPointAtDraggingStart)
    {
    	this.endPointAtDraggingStart = endPointAtDraggingStart;
    }
	}
	
	/**
	 * Stores a reference to a {@link Line} object so that its start and end point
	 * can also be stored in a {@link BehaviourMouseDraggingData} instance  on
	 * dragging start.
	 */
	public class BehaviourMouseDraggingStartLine
			extends BehaviourMouseDraggingStart
	{
		/**
		 * constructor injection
		 */
		private Line line;

		/**
		 * constructor injection
		 */
		private BehaviourMouseDraggingData behaviourMouseDraggingData;

		public BehaviourMouseDraggingStartLine(
				Line line, BehaviourMouseDraggingData behaviourMouseDraggingData)
		{
			super(line, behaviourMouseDraggingData);

			this.line = line;
			this.behaviourMouseDraggingData = behaviourMouseDraggingData;
		}

		/**
		 * propagate start and end point values of {@link #line} to {@link
		 * #behaviourMouseDraggingData}
		 *
		 * @see de.jmda.fx.node.behaviour.BehaviourMouseDraggingStart#showBehaviour(javafx.scene.input.MouseEvent)
		 */
		@Override
    public void showBehaviour(MouseEvent mouseEvent)
    {
			behaviourMouseDraggingData.setStartPointAtDraggingStart(line.getStartPoint());
			behaviourMouseDraggingData.setEndPointAtDraggingStart(line.getEndPoint());

			super.showBehaviour(mouseEvent);
    }
	}

	public class BehaviourMouseDraggingLine extends BehaviourMouseDragging
	{
		/**
		 * constructor injection
		 */
		protected Line line;

		/**
		 * constructor injection
		 */
		protected BehaviourMouseDraggingData behaviourMouseDraggingData;

		public BehaviourMouseDraggingLine(
				Line line, BehaviourMouseDraggingData behaviourMouseDraggingData)
    {
	    super(line, behaviourMouseDraggingData);

	    this.line = line;
			this.behaviourMouseDraggingData = behaviourMouseDraggingData;
    }

		@Override
	  public void showBehaviour(MouseEvent mouseEventCurrent)
	  {
			// store current mouse event coordinates
			double mouseEventCurrentX = mouseEventCurrent.getSceneX();
			double mouseEventCurrentY = mouseEventCurrent.getSceneY();
			
			// shortcut to most recent mouse coordinates
			double mouseEventMostRecentX = behaviourMouseDraggingData.getMouseDraggingMostRecent().getSceneX();
			double mouseEventMostRecentY = behaviourMouseDraggingData.getMouseDraggingMostRecent().getSceneY();
			
			BehaviourMouseDraggingConstraint lineDraggingConstraint =
					new BehaviourMouseDraggingConstraint(line);
			
			DraggingData lineDraggingDataRequest =
					new DraggingData(
							mouseEventCurrentX - mouseEventMostRecentX,
							mouseEventCurrentY - mouseEventMostRecentY);
			
			DraggingCalculator lineDraggingCalculator =
					new DraggingCalculatorDefault(
							line, lineDraggingDataRequest, lineDraggingConstraint);
			
			DraggingData lineDraggingData = lineDraggingCalculator.calculate();

			double startPointX = line.startPoint.get().getX() + lineDraggingData.getDragX();
			double startPointY = line.startPoint.get().getY() + lineDraggingData.getDragY();
			double endPointX   = line.endPoint.get().getX()   + lineDraggingData.getDragX();
			double endPointY   = line.endPoint.get().getY()   + lineDraggingData.getDragY();

			// set line start and end point to new coordinates
			line.setStartX(startPointX);
			line.setStartY(startPointY);
			line.setEndX(endPointX);
			line.setEndY(endPointY);
			
			behaviourMouseDraggingData.setMouseDragging(mouseEventCurrent);
	  }
	}
}